package net.sf.midollo.net.cgitunnel.server;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;

import net.sf.midollo.io.IOStream;
import net.sf.midollo.net.cgitunnel.CGITunnelConstants;
import net.sf.midollo.net.io.SocketIOStreamFactory;
import net.sf.midollo.net.protocol.Protocol;

public class CGITunnelServer implements Protocol, CGITunnelConstants {

  public static final int BUFFER_SIZE = 8 * 1024;

  protected IOException error;
  protected DataInputStream dis;
  protected DataOutputStream dos;
  protected IOStream ioRemote;
  protected InputStream inRemote;
  protected OutputStream outRemote;
  protected byte[] writerBuffer;
  protected byte[] readerBuffer;
  protected Thread threadReader;

  public CGITunnelServer(IOStream ioRequest) throws IOException {
    this.dis = new DataInputStream(ioRequest.getInput());
    this.dos = new DataOutputStream(ioRequest.getOutput());
    this.writerBuffer = new byte[BUFFER_SIZE];
    this.readerBuffer = new byte[BUFFER_SIZE];
    this.ioRemote = null;
    this.inRemote = null;
    this.outRemote = null;
    this.threadReader = null;
  }

  public IOException getError() {
    return this.error;
  }

  public void run() {
    try {
      this.error = null;
      while (true)
        respondToCommand();
    } catch (IOException e) {
      this.error = e;
    }
  }

  public void disconnect() throws IOException {
    if (this.ioRemote != null)
      this.ioRemote.close();
  }

  protected void respondToCommand() throws IOException {
    byte cmd = this.dis.readByte();
    switch (cmd) {
      case CMD_WRITE:
        respondToCmdWrite();
        break;
      case CMD_OPEN:
        respondToCmdOpen();
        break;
      case CMD_CLOSE:
        respondToCmdClose();
        break;
      default:
        respondToCmdUnknown(cmd);
    }
  }

  protected void respondToCmdOpen() throws IOException {
    String host = this.dis.readUTF();
    int port = this.dis.readInt();
    try {
      SocketIOStreamFactory socketIOStreamFactory = new SocketIOStreamFactory(host, port);
      this.ioRemote = socketIOStreamFactory.open();
      this.inRemote = this.ioRemote.getInput();
      this.outRemote = this.ioRemote.getOutput();
    } catch (Exception e) {
      sendException(CMD_OPEN, e);
      return;
    }
    this.threadReader = new Thread(new Reader());
    this.threadReader.start();
  }

  protected void respondToCmdClose() throws IOException {
    try {
      this.ioRemote.close();
    } catch (Exception e) {
      sendException(CMD_CLOSE, e);
      return;
    }
  }

  protected void respondToCmdWrite() throws IOException {
    int count = this.dis.readInt();
    int len;
    while (count > 0) {
      if (count > this.writerBuffer.length)
        len = this.writerBuffer.length;
      else
        len = count;
      this.dis.readFully(this.writerBuffer, 0, len);
      count -= len;
      try {
        this.outRemote.write(this.writerBuffer, 0, len);
      } catch (Exception e) {
        sendException(CMD_WRITE, e);
        return;
      }
    }
  }

  protected void respondToCmdUnknown(byte cmd) throws IOException {
    sendException((byte)0, new IOException("unknown command received: " + cmd));
  }

  protected void sendException(byte cmd, Exception e) throws IOException {
    throw new IOException("CGITunnelServer cmd=" + cmd + ' ' + e.toString());
  }

  protected boolean doRead() throws IOException {
    int nread;
    try {
      nread = this.inRemote.read(this.readerBuffer, 0, this.readerBuffer.length);
    } catch (InterruptedIOException e) {
      nread = e.bytesTransferred;
    } catch (Exception e) {
      sendException(CMD_READ, e);
      return false;
    }
    if (nread > 0)
      this.dos.write(this.readerBuffer, 0, nread);
    this.dos.flush();
    return (nread >= 0);
  }

  protected class Reader implements Runnable {

    public void run() {
      try {
        while (doRead())
          ;
      } catch (Exception e) {
      }
    }
  }
}
